﻿/*
Copyright © 2011 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Simple.Storage.Serializer
{
    /// <summary>
    /// Implements the serializer for any instance of <see cref="ICollection{T}"/>.
    /// </summary>
    public class CollectionSerializer : AbstractSerializer
    {
        /// <summary>
        /// The parent serializer used to serialize the element of the collections.
        /// </summary>
        private readonly ISerializer parent;

        /// <summary>
        /// The cache of the already available serializer.
        /// </summary>
        /// <remarks>
        /// The keys are collection types.
        /// </remarks>
        private readonly IDictionary<Type, ISerializer> cache;

        /// <summary>
        /// The cache of created serializer for each element type.
        /// </summary>
        /// <remarks>
        /// The keys are element types.
        /// </remarks>
        private readonly IDictionary<Type, ISerializer> children;

        /// <summary>
        /// The generic definition associated with <see cref="ICollection{T}"/>.
        /// </summary>
        private readonly Type definition;

        /// <summary>
        /// The generic definition associated with <see cref="CollectionSerializer.Retrieve{T}()"/>.
        /// </summary>
        private readonly MethodInfo retrieveMethod;

        /// <summary>
        /// Initializes a new instance of the CollectionSerializer class.
        /// </summary>
        /// <param name="parent">
        /// The parent serializer used to serialize the element of the collections.
        /// </param>
        public CollectionSerializer(ISerializer parent)
        {
            if (parent == null)
            {
                throw new ArgumentNullException("parent");
            }

            this.parent = parent;
            this.cache = new Dictionary<Type, ISerializer>();
            this.children = new Dictionary<Type, ISerializer>();
            this.definition = typeof(ICollection<>);
            this.retrieveMethod = typeof(CollectionSerializer).GetMethod(
                "Retrieve",
                BindingFlags.Instance | BindingFlags.NonPublic,
                null,
                Type.EmptyTypes,
                null);
        }

        /// <summary>
        /// Gets the parent serializer used to serialize the element of the collections.
        /// </summary>
        public ISerializer Parent
        {
            get { return this.parent; }
        }

        /// <summary>
        /// Retrieves or creates a <see cref="TypedCollectionSerializer{T}"/> for a
        /// specified type.
        /// </summary>
        /// <typeparam name="T">The type of the elements.</typeparam>
        /// <returns>
        /// The associated <see cref="TypedCollectionSerializer{T}"/> instance;
        /// or <c>null</c> if the <see cref="CollectionSerializer.Parent"/> can serialize
        /// <typeparamref name="T"/> elements.
        /// </returns>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1004:GenericMethodsShouldProvideTypeParameter",
            Justification = "Used internally to create TypedCollectionSerializer<T> instance")]
        protected ISerializer Retrieve<T>()
        {
            if (this.children.ContainsKey(typeof(T)))
            {
                return this.children[typeof(T)];
            }

            if (this.parent.CanSerialize(typeof(T)))
            {
                TypedCollectionSerializer<T> serializer = new TypedCollectionSerializer<T>(this.parent);
                this.children.Add(typeof(T), serializer);
                return serializer;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Retrieves of creates a <see cref="TypedCollectionSerializer{T}"/> for a
        /// specified collection type.
        /// </summary>
        /// <param name="type">The collection type.</param>
        /// <returns>The associated <see cref="TypedCollectionSerializer{T}"/> instance.</returns>
        /// <exception cref="NotSupportedException">
        /// The <paramref name="type"/> doesn't inherit from <see cref="ICollection{T}"/> interface.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The <paramref name="type"/> is a generic type definition.
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The <see cref="CollectionSerializer.Parent"/> can't serialize elements of the
        /// <paramref name="type"/> collection.
        /// </exception>
        protected ISerializer Retrieve(Type type)
        {
            // The collection type is already known
            if (this.cache.ContainsKey(type))
            {
                return this.cache[type];
            }

            // Extract the ICollection{T} interface to retrieve T
            Type elementType;
            if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == this.definition)
            {
                elementType = type.GetGenericArguments().First();
            }
            else
            {
                Type collectionInterface = type.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == this.definition);
                if (collectionInterface == null)
                {
                    // TODO: Add a message
                    throw new NotSupportedException();
                }

                elementType = collectionInterface.GetGenericArguments().First();
            }

            if (elementType.IsGenericParameter)
            {
                // TODO: Add a message
                throw new NotSupportedException();
            }

            MethodInfo generic = this.retrieveMethod.MakeGenericMethod(elementType);

            ISerializer result = (ISerializer)generic.Invoke(this, null);
            if (result == null)
            {
                string message = string.Format(Messages.Culture, Messages.SerializerUnknown, elementType);
                throw new NotSupportedException(message);
            }
            else
            {
                return result;
            }
        }

        /// <summary>
        /// Implements the <see cref="ISerializer.CanSerialize"/> logic.
        /// </summary>
        /// <param name="type">
        /// The reference type - guaranty to be not <c>null</c>.
        /// </param>
        /// <returns>
        /// <c>true</c> if the serializer can work with instances of <paramref name="type"/>;
        /// <c>false</c> otherwise.
        /// </returns>
        protected override bool InternalCanSerialize(Type type)
        {
            try
            {
                return this.Retrieve(type).CanSerialize(type);
            }
            catch (NotSupportedException)
            {
                return false;
            }
        }

        /// <summary>
        /// Implements the <see cref="ISerializer.CanWorkInPlace"/> logic.
        /// </summary>
        /// <param name="type">
        /// The reference type - guaranty to be not <c>null</c>.
        /// </param>
        /// <returns>
        /// <c>true</c> if the serializer can work with initialized instances of
        /// <paramref name="type"/>; <c>false</c> otherwise.
        /// </returns>
        protected override bool InternalCanWorkInPlace(Type type)
        {
            try
            {
                return this.Retrieve(type).CanWorkInPlace(type);
            }
            catch (NotSupportedException)
            {
                return false;
            }
        }

        /// <summary>
        /// Implements the serialization logic.
        /// </summary>
        /// <param name="writer">
        /// The writer - guaranty to be not <c>null</c>.
        /// </param>
        /// <param name="type">
        /// The reference type - guaranty to be not <c>null</c>.
        /// </param>
        /// <param name="value">
        /// The value to serialize - guaranty to be not <c>null</c> and associated with
        /// the current serializer.
        /// </param>
        protected override void InternalSerialize(BinaryWriter writer, Type type, object value)
        {
            this.Retrieve(type).Serialize(writer, type, value);
        }

        /// <summary>
        /// Implements the deserialization logic.
        /// </summary>
        /// <param name="reader">
        /// The reader - guaranty to be not <c>null</c>.
        /// </param>
        /// <param name="type">
        /// The reference type - guaranty to be not <c>null</c>.
        /// </param>
        /// <returns>The deserialized value.</returns>
        protected override object InternalDeserialize(BinaryReader reader, Type type)
        {
            return this.Retrieve(type).Deserialize(reader, type);
        }

        /// <summary>
        /// Implements the in-place deserialization logic.
        /// </summary>
        /// <param name="reader">
        /// The reader - guaranty to be not <c>null</c>.
        /// </param>
        /// <param name="type">
        /// The reference type - guaranty to be not <c>null</c>.
        /// </param>
        /// <param name="value">
        /// The container for the deserialized object - guaranty to be not <c>null</c>.
        /// </param>
        protected override void InternalDeserialize(BinaryReader reader, Type type, object value)
        {
            this.Retrieve(type).Deserialize(reader, type, value);
        }
    }
}
